Skip to content

fix: SSE reconnect + status sync + logs + Advanced + webhook + Swarm - #9

Merged
MakFly merged 7 commits into
mainfrom
fix/sse-status-swarm
May 10, 2026
Merged

fix: SSE reconnect + status sync + logs + Advanced + webhook + Swarm#9
MakFly merged 7 commits into
mainfrom
fix/sse-status-swarm

Conversation

@MakFly

@MakFly MakFly commented May 10, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the cluster of bugs observed in prod plus introduces a Docker Swarm runtime for zero-downtime control-plane updates.

  • SSE reconnect (events-provider.tsx): auto-reconnect with backoff + token refresh; new 3-state status (connecting/open/reconnecting/offline) replaces the silent boolean.
  • Status sync (app-runtime.ts + app-status-reconciler.ts): UI fallback to highest-priority snapshot when stored containerId is stale; API refreshes apps.container_id on label-based match.
  • Logs 404 (apps.ts + use-log-stream.ts): convention path fallback before 404; 1s grace before WS to REST fallback.
  • Advanced tab (advanced.tsx): dark-mode variants on the orange alert + collapsible Examples block with three copy-paste Caddy snippets.
  • Webhook bytea (webhook-deliveries.ts): postgres.js Buffer bind crash that aborted the whole webhook handler. Coerce Buffer to Uint8Array via toDriver.
  • Swarm runtime (installer): new mode that deploys api/web with replicas 2 + start-first rolling updates. Watchtower replaced by a 5-min systemd timer running docker stack deploy.

Test plan

  • typecheck, lint, spdx all green
  • test suite: 70 fail same as main baseline (no regression introduced)
  • Post-merge webhook redelivery to fixture-symfony-api triggers rebuild
  • Optional follow-up: re-install with --runtime swarm on the VPS

MakFly added 7 commits May 10, 2026 14:56
The native EventSource retry stops on non-2xx responses (typically a 401
expired cookie), so the SSE banner stayed stuck on 'Stream error' until
the user manually reloaded. We now intercept readyState=CLOSED in onerror,
schedule our own reconnect with exponential backoff (capped at 30s), and
refresh the access cookie before each attempt.

Banner now exposes a 3-state status (connecting | open | reconnecting |
offline) instead of a boolean, so the UI shows 'Live reconnecting…' during
the recovery window.

Signed-off-by: kevin <kev.aubree@gmail.com>
When a container is recreated (blue/green app deploy, host restart,
watchtower swap) the stored apps.container_id no longer matches the live
agent ID/name, so selectAppSnapshot returned null and the UI showed
'Stopped' on healthy apps. Two-sided fix:

- API reconciler persists the new canonical reference whenever the
  label-based lookup finds a different container than the stored one.
- Front falls back to the highest-priority snapshot for the app_id when
  the strict expectedRef match fails — covers the brief window between
  container recreation and the next /apps poll.

Signed-off-by: kevin <kev.aubree@gmail.com>
Two improvements to stop the 'Failed to load logs (404)' banner on the
deployments tab:

- API: when build.log_path is null (legacy row, killed worker), try the
  convention path ${PLOYDOK_BUILD_DIR}/<appId>/<buildId>.log before giving
  up. Return 200 with '(no logs captured)' body when truly empty so the UI
  doesn't surface a misleading 404.
- Front: defer the WS→REST fallback by 1s. Builds that just finished can
  race the DB write of log_path; the delay lets the worker persist before
  we hit the archive endpoint.

Signed-off-by: kevin <kev.aubree@gmail.com>
The orange warning alert had no dark: variants and rendered as a white-on-
white block in dark mode. Added dark variants, reworded the message to
explain the merge-into-route-handler behaviour and the server-side
validation safety net, and added a collapsible 'Examples' block with three
copy-paste snippets (HSTS header, redirect, URI rewrite) so first-time
users have something to start from beyond the upstream Caddy docs link.

Signed-off-by: kevin <kev.aubree@gmail.com>
Adds --runtime=swarm (default) that deploys api/web as Swarm services with
replicas: 2 + update_config.order: start-first + healthcheck-gated cutover.
A new systemd timer (ploydok-update.timer, 5-min cadence) re-runs
`docker stack deploy --resolve-image always` so a freshly published
:edge image rolls out without downtime. Watchtower is gone — Swarm's
update_config replaces it with proper health gating and rollback.

- installer/install.sh: --runtime flag, ensure_swarm() that auto-inits a
  single-node Swarm if needed, db_migrate handles both runtimes.
- docker-stack.yml: new template with deploy: blocks, host-mode ports on
  Caddy (so ACME's same-source-IP rule still holds), buildkit switched to
  the non-rootless image since Swarm ignores security_opt.
- ploydok.service: ExecStart now `docker stack deploy` (idempotent).
- ploydok-update.{service,timer}: cron equivalent of watchtower for Swarm.
- docker-compose.yml: watchtower removed (legacy compose mode now needs
  ploydok-cli upgrade for updates).
- getting-started.md: Swarm vs Compose section + bascule procedure.

Signed-off-by: kevin <kev.aubree@gmail.com>
The custom bytea type relied on Drizzle's default driver path, but
postgres.js (Bun) sees the Buffer as a plain Object during prepared
statement bind and crashes with 'string argument must be of type string
or instance of Buffer or ArrayBuffer'. The whole webhook handler aborted
mid-flight: webhook signature was verified, push event was matched, then
the audit insert into webhook_deliveries killed the request — so the
deploy job never reached BullMQ.

Fix: add toDriver() on the bytea customType that returns a Uint8Array
view over the same memory, which postgres.js binds correctly.

Signed-off-by: kevin <kev.aubree@gmail.com>
The CI dry-run installer test asserts /opt/ploydok/docker-compose.yml
exists, which the swarm-default writes to docker-stack.yml instead.
Defaulting to swarm also silently swarm-init's any host that runs the
installer, which is a behaviour change too big for this PR. Make
--runtime=swarm explicit opt-in for now; compose stays the default.

Signed-off-by: kevin <kev.aubree@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a421ece478

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# is restarted reconciles the stack to the current docker-stack.yml without
# tearing existing services down. The first invocation provisions the stack;
# subsequent ones (e.g. after a reboot) are essentially no-ops.
ExecStart=/usr/bin/docker stack deploy --resolve-image always --with-registry-auth --prune -c ${PLOYDOK_INSTALL_DIR}/docker-stack.yml ploydok

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore compose systemd unit when runtime is compose

The --runtime=compose path still writes ploydok.service, but this template now always executes docker stack deploy against docker-stack.yml. In compose mode, ensure_swarm is skipped and only docker-compose.yml is rendered, so systemctl restart ploydok.service will fail on hosts not initialized as Swarm (and may also fail due to missing docker-stack.yml). This regresses the advertised fallback runtime and makes --runtime=compose non-functional.

Useful? React with 👍 / 👎.

Comment on lines +38 to +42
- target: 3335
published: 3335
host_ip: 127.0.0.1
protocol: tcp
mode: host

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove host-published fixed API port with 2 replicas

This service publishes API with mode: host and a fixed published: 3335 while also setting deploy.replicas: 2. Docker Swarm documents that mode=host + fixed published port allows only one task per node, so on the default single-node Swarm install one replica cannot be scheduled and the service will remain degraded (breaking the intended zero-downtime two-replica design).

Useful? React with 👍 / 👎.

@MakFly
MakFly merged commit cf7357e into main May 10, 2026
4 checks passed
@MakFly
MakFly deleted the fix/sse-status-swarm branch May 10, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant